﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using Windows.Devices.Enumeration;
using Windows.Storage.Streams;
using Windows.Devices.Bluetooth;
using Windows.Devices.Enumeration.Pnp;
using System.Runtime.InteropServices.WindowsRuntime;

namespace BeanServices
{
    public delegate void ScratchChangedCompletedHandler(ScratchData scratchData);
    public delegate void DeviceConnectionUpdatedHandler(bool isConnected);

    /// <summary>
    /// This is a simple Data Message Class used to pass the data received via events. 
    /// </summary>
    public class ScratchData
    {
        public byte[] Data { get; private set; }

        public ScratchData(byte[] data)
        {
            Data = data;
        }
    }

    public class BeanScratchService
    {
        protected DeviceInformationCollection _devices;
        protected GattDeviceService _service;
        protected PnpObjectWatcher _watcher;
        protected String _deviceContainerId;

        /// <summary>
        /// ID for the ScratchService and Characteristics 
        /// </summary>
        private Guid _scratchServiceID = new Guid("a495ff20-c5b1-4b44-b512-1370f02d74de");
        private Guid _scratch1CharacteristicID = new Guid("a495ff21-c5b1-4b44-b512-1370f02d74de");
        private Guid _scratch2CharacteristicID = new Guid("a495ff22-c5b1-4b44-b512-1370f02d74de");
        private Guid _scratch3CharacteristicID = new Guid("a495ff23-c5b1-4b44-b512-1370f02d74de");
        private Guid _scratch4CharacteristicID = new Guid("a495ff24-c5b1-4b44-b512-1370f02d74de");
        private Guid _scratch5CharacteristicID = new Guid("a495ff25-c5b1-4b44-b512-1370f02d74de");

        private GattCharacteristic _characteristic1;
        private GattCharacteristic _characteristic2;
        private GattCharacteristic _characteristic3;
        private GattCharacteristic _characteristic4;
        private GattCharacteristic _characteristic5;

        /// <summary>
        /// True if list of paired BLE scratch services is available. If not, then bluetooth is disabled or
        /// no scratch services is paired
        /// </summary>
        public bool DevicesAreAvailable { get; set; }
        /// <summary>
        /// Event that is raised if Bean connection status is changed
        /// </summary>
        public event DeviceConnectionUpdatedHandler DeviceConnectionUpdated;
        /// <summary>
        /// Event that is raised if data is written to Scratch1 on the Bean
        /// </summary>
        public event ScratchChangedCompletedHandler Scratch1ChangeCompleted;
        /// <summary>
        /// Event that is raised if data is written to Scratch2 on the Bean
        /// </summary>
        public event ScratchChangedCompletedHandler Scratch2ChangeCompleted;
        /// <summary>
        /// Event that is raised if data is written to Scratch3 on the Bean
        /// </summary>
        public event ScratchChangedCompletedHandler Scratch3ChangeCompleted;
        /// <summary>
        /// Event that is raised if data is written to Scratch4 on the Bean
        /// </summary>
        public event ScratchChangedCompletedHandler Scratch4ChangeCompleted;
        /// <summary>
        /// Event that is raised if data is written to Scratch5 on the Bean
        /// </summary>
        public event ScratchChangedCompletedHandler Scratch5ChangeCompleted;
        /// <summary>
        /// Returns the UUID for the Scratch Service
        /// </summary>
        public Guid ServiceID
        {
            get { return _scratchServiceID; }
        }

        public BeanScratchService() {}

        #region Enumerate scratch services devices and initialize a gatt service

        /// <summary>
        /// Refreshes the list of BLE devices containing Scratch services
        /// </summary>
        private async Task RefreshListOfDevices()
        {
            _devices = await DeviceInformation.FindAllAsync(GattDeviceService.GetDeviceSelectorFromUuid(ServiceID), new string[] { "System.Devices.ContainerId" });
            DevicesAreAvailable = _devices.Count > 0;
        }

        /// <summary>
        /// Refresh list of scractch service devices and return the list
        /// </summary>
        /// <returns>Collection of devices</returns>
        public async Task<DeviceInformationCollection> GetListOfDevices()
        {
            await RefreshListOfDevices();
            return _devices;
        }

        public async Task InitializeService(DeviceInformation deviceInfo)
        {
            _deviceContainerId = "{" + deviceInfo.Properties["System.Devices.ContainerId"] + "}";
            StartDeviceConnectionWatcher();

            _service = await GattDeviceService.FromIdAsync(deviceInfo.Id);
            if (_service != null)
            {
                InitializeCharacteristics();
                await RegisterNotificationsAsync();
            }

            bool isConnected = await IsDeviceConnected();
            if (DeviceConnectionUpdated != null)
            {
                DeviceConnectionUpdated(isConnected);
            }
        }

        #endregion

        #region Setup PnP Watcher object to handle connection state

        public async Task<bool> IsDeviceConnected()
        {
            var deviceObject = await PnpObject.CreateFromIdAsync(PnpObjectType.DeviceContainer, _deviceContainerId, new string[] { "System.Devices.Connected" });
            bool isConnected;
            if (!Boolean.TryParse(deviceObject.Properties["System.Devices.Connected"].ToString(), out isConnected))
                return false;
            return isConnected;
        }

        /// <summary>
        /// Register to be notified when a connection is established to the Bluetooth device
        /// </summary>
        private void StartDeviceConnectionWatcher()
        {
            // Create watcher
            _watcher = PnpObject.CreateWatcher(PnpObjectType.DeviceContainer,
                new string[] { "System.Devices.Connected" }, String.Empty);

            _watcher.Updated += DeviceConnection_Updated;
            _watcher.Start();
        }

        /// <summary>
        /// Invoked when a connection a device is updated
        /// </summary>
        /// <param name="sender">The watcher object that sent the notification</param>
        /// <param name="args">The updated device object properties</param>
        private async void DeviceConnection_Updated(PnpObjectWatcher sender, PnpObjectUpdate args)
        {
            if (_deviceContainerId == args.Id)
            {
                await RegisterNotificationsAsync();

                bool isConnected = await IsDeviceConnected();
                if (DeviceConnectionUpdated != null)
                {
                    DeviceConnectionUpdated(isConnected);
                }
            }
        }

        #endregion

        #region Initialize Characteristics and setup notifications

        /// <summary>
        /// Initializes the scratch service characteristics
        /// </summary>
        /// <param name="deviceInfo">DeviceInformation for device to be initialized</param>
        protected void InitializeCharacteristics()
        {
            _characteristic1 = _service.GetCharacteristics(_scratch1CharacteristicID)[0];
            _characteristic1.ProtectionLevel = GattProtectionLevel.EncryptionRequired;
            _characteristic1.ValueChanged += Characteristic_ValueChanged;

            _characteristic2 = _service.GetCharacteristics(_scratch2CharacteristicID)[0];
            _characteristic2.ProtectionLevel = GattProtectionLevel.EncryptionRequired;
            _characteristic2.ValueChanged += Characteristic_ValueChanged;

            _characteristic3 = _service.GetCharacteristics(_scratch3CharacteristicID)[0];
            _characteristic3.ProtectionLevel = GattProtectionLevel.EncryptionRequired;
            _characteristic3.ValueChanged += Characteristic_ValueChanged;

            _characteristic4 = _service.GetCharacteristics(_scratch4CharacteristicID)[0];
            _characteristic4.ProtectionLevel = GattProtectionLevel.EncryptionRequired;
            _characteristic4.ValueChanged += Characteristic_ValueChanged;

            _characteristic5 = _service.GetCharacteristics(_scratch5CharacteristicID)[0];
            _characteristic5.ProtectionLevel = GattProtectionLevel.EncryptionRequired;
            _characteristic5.ValueChanged += Characteristic_ValueChanged;
        }

        /// <summary>
        /// Register notification for characteristics on device
        /// </summary>
        protected async Task RegisterNotificationsAsync()
        {
            GattCommunicationStatus status = GattCommunicationStatus.Unreachable;
            if (_characteristic1 != null)
            {
                var result = await _characteristic1.ReadValueAsync(BluetoothCacheMode.Uncached);
                status = result.Status;
            }

            if (status == GattCommunicationStatus.Success)
            {
                await _characteristic1.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
                await _characteristic2.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
                await _characteristic3.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
                await _characteristic4.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
                await _characteristic5.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
            }
        }

        /// <summary> 
        /// Invoked when Windows receives data from Bluetooth device. 
        /// </summary> 
        /// <param name="sender">The GattCharacteristic object whose value is received.</param> 
        /// <param name="args">The new characteristic value sent by the device.</param> 
        private void Characteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
        {
            ScratchData dataMessage = new ScratchData(args.CharacteristicValue.ToArray());

            if (sender.Uuid.Equals(_scratch1CharacteristicID))
                if (Scratch1ChangeCompleted != null)
                    Scratch1ChangeCompleted(dataMessage);
            if (sender.Uuid.Equals(_scratch2CharacteristicID))
                if (Scratch2ChangeCompleted != null)
                    Scratch2ChangeCompleted(dataMessage);
            if (sender.Uuid.Equals(_scratch3CharacteristicID))
                if (Scratch3ChangeCompleted != null)
                    Scratch3ChangeCompleted(dataMessage);
            if (sender.Uuid.Equals(_scratch4CharacteristicID))
                if (Scratch4ChangeCompleted != null)
                    Scratch4ChangeCompleted(dataMessage);
            if (sender.Uuid.Equals(_scratch5CharacteristicID))
                if (Scratch5ChangeCompleted != null)
                    Scratch5ChangeCompleted(dataMessage);
        }

        #endregion

        #region Write to scratch characteristics

        protected async Task WriteDataAsync(GattCharacteristic characteristic, ScratchData message)
        {
            try
            {
                var status = await characteristic.WriteValueAsync(message.Data.AsBuffer());
                if (status == GattCommunicationStatus.Unreachable)
                {
                    throw new System.Exception("Write to characteristic failed.");
                }
            }
            catch (Exception e)
            {
                throw new System.Exception("ERROR: Accessing your device failed.", e);
            }
        }

        /// <summary>
        /// Write data to bean Scratch characteristic 1
        /// </summary>
        /// <param name="message">Data to write</param>
        public async Task WriteScratch1Async(ScratchData data)
        {
            await WriteDataAsync(_characteristic1, data);
        }

        /// <summary>
        /// Write data to bean Scratch characteristic 2
        /// </summary>
        /// <param name="message">Data to write</param>
        public async Task WriteScratch2Async(ScratchData data)
        {
            await WriteDataAsync(_characteristic2, data);
        }

        /// <summary>
        /// Write data to bean Scratch characteristic 3
        /// </summary>
        /// <param name="message">Data to write</param>
        public async Task WriteScratch3Async(ScratchData data)
        {
            await WriteDataAsync(_characteristic3, data);
        }

        /// <summary>
        /// Write data to bean Scratch characteristic 4
        /// </summary>
        /// <param name="message">Data to write</param>
        public async Task WriteScratch4Async(ScratchData data)
        {
            await WriteDataAsync(_characteristic4, data);
        }

        /// <summary>
        /// Write message to bean Scratch characteristic 5
        /// </summary>
        /// <param name="message">Data to write</param>
        public async Task WriteScratch5Async(ScratchData message)
        {
            await WriteDataAsync(_characteristic5, message);
        }

        #endregion

        #region Read from scratch charecteristics

        protected async Task<ScratchData> ReadDataAsync(GattCharacteristic characteristic)
        {
            try
            {
                var result = await characteristic.ReadValueAsync(BluetoothCacheMode.Uncached);
                if (result.Status == GattCommunicationStatus.Success)
                {
                    return new ScratchData(result.Value.ToArray());
                }
                else
                {
                    throw new System.Exception("Read characteristic failed.");
                }
            }
            catch (Exception e)
            {
                throw new System.Exception("ERROR: Accessing your device failed.", e);
            }
        }

        /// <summary>
        /// Read data from scracth characteristic 1
        /// </summary>
        /// <returns>data to return</returns>
        public async Task<ScratchData> ReadScratch1Async()
        {
            return await ReadDataAsync(_characteristic1);
        }

        /// <summary>
        /// Read data from scracth characteristic 2
        /// </summary>
        /// <returns>data to return</returns>
        public async Task<ScratchData> ReadScratch2Async()
        {
            return await ReadDataAsync(_characteristic2);
        }

        /// <summary>
        /// Read data from scracth characteristic 3
        /// </summary>
        /// <returns>data to return</returns>
        public async Task<ScratchData> ReadScratch3Async()
        {
            return await ReadDataAsync(_characteristic3);
        }

        /// <summary>
        /// Read data from scracth characteristic 4
        /// </summary>
        /// <returns>data to return</returns>
        public async Task<ScratchData> ReadScratch4Async()
        {
            return await ReadDataAsync(_characteristic4);
        }

        /// <summary>
        /// Read data from scracth characteristic 5
        /// </summary>
        /// <returns>data to return</returns>
        public async Task<ScratchData> ReadScratch5Async()
        {
            return await ReadDataAsync(_characteristic5);
        }

        #endregion
    }
}
